namespace Patterns4Net.Core.CecilExtensions
{
    using System;
    using System.Diagnostics.Contracts;
    using System.IO;
    using System.Reflection;
    using Mono.Cecil;
    using Patterns4Net.Attributes;
    using Patterns4Net.Attributes.DesignPatterns;

    /// <summary>
    /// Provides extension methods to translate Cecil's data structure to their 
    /// <see cref="System.Reflection"/> counterparts.
    /// </summary>
    public static class CecilToReflection
    {
        /// <summary>
        /// For given <see cref="TypeReference"/> returns corresponding <see cref="Type"/>.
        /// Try to get avoid using this method and rather use Cecil's type structures directly 
        /// if possible.
        /// </summary>
        /// <param name="typeReference">The Cecil type definition data structure.</param>
        /// <returns>Corresponding <see cref="System.Type"/>.</returns>
        public static Type GetSystemType(this TypeReference typeReference)
        {
            Contract.Requires(typeReference != null);
            Contract.Ensures(Contract.Result<Type>() != null);
            Contract.Assume(typeReference.Module != null);
            Contract.Assume(typeReference.Module.Assembly != null);

            // typeDefinition contains meta-data of a type that might have not been loaded
            // so we need to load it's assembly using System.Reflection
            Assembly loadedAssembly = LoadReflectionAssembly(typeReference);

            // Now we can obtain outr type's Type instance from loaded assembly object
            Contract.Assume(typeReference.FullName != null);
            var reflectionName = GetReflectionTypeName(typeReference.FullName);
            Type result = loadedAssembly.GetType(reflectionName);

            if (result == null)
            {
                throw new ConversionException(
                    typeReference, 
                    string.Format("Assembly.GetType called with parameter '{0}' returned null reference", reflectionName));
            }

            return result;
        }

        /// <summary>
        /// Checks whether given types are equal.
        /// </summary>
        /// <remarks>
        ///     <para>
        ///     This method does not load <param name="type"></param> representation of Cecil, 
        ///     it does string comparison of type names.
        ///     </para>
        /// </remarks>
        /// <returns><c>true</c> if the types are equal.</returns>
        public static bool IsEqual(this TypeReference typeReference, Type type)
        {
            Contract.Requires(typeReference != null);
            Contract.Requires(type != null);
            Contract.Assert(type.Name != null);

            // Reflection uses plus for nested types, Cecil uses slash
            var cecilTypeName = ReflectionToCecil.GetCecilTypeName(type.FullName);
            if (typeReference.FullName == cecilTypeName)
            { 
                return true;
            }

            if (typeReference is GenericInstanceType &&
                type.IsGenericTypeDefinition == false &&
                type.IsGenericType)
            {
                if (IsGenericTypeEqual(typeReference, type) == false)
                {
                    return false;
                }

                var genericRef = (GenericInstanceType)typeReference;
                var reflectionGenericArgs = type.GetGenericArguments();
                if (genericRef.GenericArguments.Count != reflectionGenericArgs.Length)
                {
                    return false;
                }

                return genericRef.GenericArguments.All((t, i) => t.IsEqual(reflectionGenericArgs[i]));
            }

            return false;
        }

        public static string GetReflectionTypeName(string cecilTypeName)
        {
            Contract.Requires(cecilTypeName != null);
            Contract.Ensures(Contract.Result<string>() != null);
            // Cecil uses slash for nested types, reflection uses plus
            return cecilTypeName.Replace("/", "+");
        }

        internal static bool IsGenericTypeEqual(this TypeReference typeReference, Type type)
        {
            int firstIdx = typeReference.FullName.IndexOf("`");
            int secondIdx = type.FullName.IndexOf("`");
            if (firstIdx < 0 || secondIdx < 0)
            {
                return false;
            }

            var firstName = typeReference.FullName.Substring(0, firstIdx - 1);
            var secondName = ReflectionToCecil.GetCecilTypeName(type.FullName.Substring(0, secondIdx - 1));
            if (firstName != secondName)
            {
                return false;
            }

            return true;
        }

        private static Assembly LoadReflectionAssembly(TypeReference typeReference)
        {
            Contract.Requires(typeReference != null);
            Contract.Assume(typeReference.Module != null);
            Contract.Assume(typeReference.Module.Assembly != null);

            var assemblyDefinition = typeReference.Module.Assembly;
            Contract.Assert(assemblyDefinition != null);
            Assembly loadedAssembly;
            try
            {                
                loadedAssembly = Assembly.Load(assemblyDefinition.ToString());
            }
            catch (ArgumentException exception)
            {
                string message =
                    string.Format(
                        "Mono.Cecil provide assembly name of this value '{0}' which is probably incorrect format.", 
                        assemblyDefinition);
                throw new ConversionException(typeReference, message, exception);
            }
            catch (IOException exception)
            {
                string message =
                    "File of assembly of given type couldn't be loaded. It is probably not in GAC nor in application directory";
                throw new ConversionException(typeReference, message, exception);
            }
            catch (BadImageFormatException exception)
            {
                throw new ConversionException(typeReference, "Assembly file is probably corrupted.", exception);
            }

            if (loadedAssembly == null)
            {
                throw new ConversionException(typeReference, "Result of Assembly.Load call is null reference.");
            }

            return loadedAssembly;
        }

        [Immutable]
        public class ConversionException : ApplicationException
        {
            private static readonly string MessageTemplate =
                "Patterns4Net Internal Error: Mono.Cecil.TypeDefinition with FullName equal to {0} cannot " +
                "be converted to corresponding System.Type. Additional info: {1}";

            public ConversionException(TypeReference typeReference, string info = "none", Exception inner = null)
                : base(string.Format(MessageTemplate, typeReference.FullName, info), inner)
            {
                Contract.Requires(typeReference != null);
                this.TypeReference = typeReference;
            }

            public TypeReference TypeReference { get; private set; }
        }
    }
}
